Typecho ≤ 1.3.0 的 Pingback 服务存在未认证服务端请求伪造( SSRF )漏洞,编号 CVE-2026-7025 , CVSS 3.1 评分 7.3 (高危)。
攻击者无需任何凭证,仅凭一个 HTTP 请求,即可让目标服务器:

  1. 向任意 URL 发起出站请求(第一跳);
  2. 解析第一跳响应中的 X-Pingback 头,再向任意指定地址发起 XML-RPC POST (第二跳)——该跳对目标地址不做任何内网校验。

漏洞由两个缺陷叠加构成:一个 PHP 弱类型比较绕过(进入漏洞路径的门票)+ 一处 出站请求缺少协议与地址白名单( SSRF 的实质)。两者缺一不可。
本文覆盖:漏洞定位、根因源码级分析、完整复现过程、真实带外证据、影响评估,以及可直接套用的修复补丁(含自动打补丁脚本)。

漏洞概览

项目内容
漏洞编号CVE-2026-7025
漏洞类型未认证服务端请求伪造(Unauthenticated SSRF)
影响版本Typecho ≤ 1.3.0
危害组件var/Widget/Service.php → Service::sendPingHandle()
触发入口POST /action/service?do=ping (或 /index.php/action/service?do=ping )
利用难度低(单次 HTTP 请求,默认配置即可触发)
CVSS 3.17.3 High(AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:L)
官方补丁披露时厂商未响应,无可用补丁

影响面:

所有使用默认配置的 Typecho ≤ 1.3.0 站点。Typecho 是国内外广泛使用的中文轻量博客系统,存量站点基数大。

背景:Typecho 的 Pingback 服务

要理解这个漏洞,先要理解 pingback 是什么。
Pingback 是一种博客间的引用通知机制(源自 WordPress 规范)。当 A 站的文章引用了 B 站的文章,A 站会向 B 站发一个通知:"嘿,我引用你了。" B 站收到后会去 A 站核实,核实通过则在 A 站文章的评论区留下一条 pingback 记录。
Typecho 实现这套机制时,把服务端能力拆成了两个动作、两个端点:

端点职责方向
/action/xmlrpc ( Service::action() 中的 xmlrpc 分支)接收别人的 pingback 通知入站
/action/service?do=ping ( Service::sendPingHandle() )发送 pingback 通知出站

漏洞出在发送端。而发送端为了完成"核实引用"这一步,需要主动去访问外部 URL——这就天然构成了一条 SSRF 通道。

漏洞原理:

漏洞的详情请参照: Typecho 1.3.0 (CVE-2026-7025)SSRF漏洞分析与修复

修复方案

立即缓解(1 分钟,不改代码)

前提认知:Typecho 后台没有任何开关能关掉这条路径。入口是无条件注册的。因此唯一能立刻生效的是网络层封禁。
Nginx(放进 server {} 块,必须在 PHP location 之前):

location ~ ^/(index\.php/)?action/service {
    deny all;
    return 403;
}

Apache(.htaccess):

RewriteRule ^(index\.php/)?action/service - [F,L]


坑一:位置错了就不生效。 这是正则 location。nginx 在同为正则的 location 之间取最先匹配到的那一条。所以它必须写在 location ~ \.php 前面。放后面等于没写,你会以为修好了其实没有。
坑二:必须同时覆盖两种入口形式。
location ~ ^/(index\.php/)?action/service
这一条正则同时覆盖 /action/service 和 /index.php/action/service 。少拦一种都白搭——实际攻击用的就是带 index.php 的那种,因为它会直接命中 location ~ \.php ,绕过 location / 里的任何判断。
副作用(必须知道):封掉 /action/service 会连带禁用站内正常的 pingback/trackback 发送,以及依赖 do=async 的异步调用。因为站内发送走的是公网域名(经 CDN 回源),源 IP 不是 127.0.0.1 ,无法用白名单放行。

  • 不在乎 pingback (现代博客基本没人用)→ 用封禁,立刻止血
  • 要保留站内功能 → 用下面的代码补丁, /action/service 可以保留

代码修复(推荐,根治)

核心思路:在 Http\Client::send() 这一收敛点一次性封死所有出站。这样 pingback 第一跳、第二跳、trackback 全都受控。
修复点 1:token 严格比较
文件: var/Typecho/Common.php → timeTokenValidate()

// 修复前
if (sha1($secret . '&' . $i) == $token) {

// 修复后
if (!is_string($token) || '' === $token) {
    return false;
}
// ...
if (hash_equals(sha1($secret . '&' . $i), $token)) {

要点:== 换成 hash_equals() 严格且恒定时间,顺带防时序攻击);先用 is_string() 挡掉 true 、数组、数字等所有非字符串输入。
这个改动同时修复 asyncHandle() ,因为它用的是同一个校验函数。
修复点 2:出站请求协议 + 地址白名单
文件: var/Typecho/Http/Client.php → send()
改动 A —— URL 解析后,加协议与地址校验:

$params = parse_url($url);

// 新增:仅允许 http/https
$scheme = strtolower($params['scheme'] ?? '');
$host   = $params['host'] ?? '';

if (!in_array($scheme, ['http', 'https'], true)) {
    throw new Exception('Forbidden scheme: ' . $scheme, 403);
}

// 新增:复用框架自带的内网校验
if (empty($host) || !Common::checkSafeHost($host)) {
    throw new Exception('Forbidden host: ' . $host, 403);

改动 B —— cURL 初始化时,锁定协议白名单:

curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $this->method);

// 新增:协议层封死 gopher/file/dict 等,并禁止协议跳转与重定向跟随
if (defined('CURLOPT_PROTOCOLS_STR')) {
    curl_setopt($ch, CURLOPT_PROTOCOLS_STR, 'http,https');
    curl_setopt($ch, CURLOPT_REDIR_PROTOCOLS_STR, 'http,https');
} else {
    curl_setopt($ch, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
    curl_setopt($ch, CURLOPT_REDIR_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
}
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false);

这里体现了复用 checkSafeHost() 的价值——它已经处理好了 127.0.0.0/8、10/8、172.16/12、192.168/16、169.254/16、0.0.0.0/8 以及 IPv6 回环/私网,不需要自己重写。
修复点 3(可选,纵深防御)
sendPingHandle() 的第二跳再加一道独立校验。修复点 2 已覆盖全部出站,这一步只是双保险:

if (!empty($xmlrpcUrl)) {
    $xp = parse_url($xmlrpcUrl);
    if (
        empty($xp['host'])
        || !in_array(strtolower($xp['scheme'] ?? ''), ['http', 'https'], true)
        || !Common::checkSafeHost($xp['host'])
    ) {
        continue;
    }
    $xmlrpc = new \IXR\Client($xmlrpcUrl);
    // ...
}

改完怎么验证

对照实验(三步):

请求修复前修复后
token: true,pingback 指向 http://127.0.0.1:6379/200403
合法 token(后台发评论时自动生成)200200(不回归)
合法 token, pingback 指向 http://169.254.169.254/latest/meta-data/服务器发起请求被 checkSafeHost 拒绝

漏洞复盘

这个漏洞的技术含量并不高,但它的成因非常典型,值得记住三点:

  • 第一,弱类型比较是一类长期存在的坑。 PHP 8 虽然收紧了部分松散比较的语义,但 string == true 这类方向依然成立。凡是把用户输入丢进 == 的地方,都要警惕。正确做法是 === 或 hash_equals() ,并且先做类型检查。
  • 第二,"有锁不用"比"没有锁"更普遍。 框架里明明躺着内网地址校验函数,却在关键路径上忘了调用。这类问题的根因不是能力缺失,而是安全能力没有和业务路径对齐。所以做安全审计时,除了问"有没有防护函数",更要问"这条路径上它被调了吗"。
  • 第三,收敛点决定了修复成本。 因为所有出站都经过 Http\Client::send() ,一个补丁就能封死全部攻击面。反过来看,如果每个调用点各自实现 HTTP 请求,修复就得改十处、还容易漏。设计时把横切关注点(网络出口、鉴权、日志)收敛到单一入口,安全成本会指数级下降。

最后,一个方法论提醒:验证 SSRF 时别迷信时间差。 只要目标在出站前提前 flush 了响应,时间差就完全失效。这时候必须上带外通道(OOB)。本文的实证正是靠这条思路才拿到了无法伪造的铁证。